package com.edgar.plugin;

import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageParser;
import android.content.res.CompatibilityInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.ArrayMap;
import android.util.Log;

import com.edgar.plugin.compat.ActivityThreadCompat;
import com.edgar.plugin.hooks.ActivityManagerHook;
import com.edgar.plugin.hooks.HandleCallbackHook;
import com.edgar.plugin.hooks.Hook;
import com.edgar.plugin.hooks.HookInstrumentation;
import com.edgar.plugin.hooks.PackageManagerHook;
import com.edgar.plugin.pm.IPluginInstallObserver;
import com.edgar.plugin.pm.PackageParserCompat;
import com.edgar.plugin.pm.Plugin;
import com.edgar.plugin.utils.FieldUtils;
import com.edgar.plugin.utils.MethodUtils;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Created by Edgar on 2018/2/8.
 */
public class PluginManager {

    private static final String TAG = "PluginManager";
    public static final int INSTALL_FILE_NO_INVALID = -1;
    public static final int INSTALL_FAILED_OLDER_SDK = -12;
    public static final int INSTALL_SUCCEEDED = 1;
    private static final int MSG_INSTALL = 0;

    private Context mHostContext;
    private PackageHandler mHandler;
    private List<Hook> mHooks = new ArrayList<>();
    private final Map<String,Plugin> mPluginPackages = new ArrayMap<>();
    private Handler mUiHandler = new Handler(Looper.getMainLooper());
    private Instrumentation mHookInstrumentation;
    private Object mActivityThread;
    private volatile static PluginManager sInstance;

    private class InstallParams{
        private File apkFile;
        private IPluginInstallObserver packageInstallObserver;

        private InstallParams(File apkFile,IPluginInstallObserver observer){
            this.apkFile = apkFile;
            this.packageInstallObserver = observer;
        }

        public void startInstall(){
            installPackageInternal(this);
        }
    }

    private class PackageHandler extends Handler{
        private PackageHandler(Looper looper){
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case MSG_INSTALL:
                    InstallParams installParams = (InstallParams) msg.obj;
                    installParams.startInstall();
                    break;
            }
        }
    }

    public static PluginManager getInstance(){
        if (sInstance == null){
            synchronized (PluginManager.class){
                if (sInstance == null){
                    sInstance = new PluginManager();
                }
            }
        }
        return sInstance;
    }

    private PluginManager(){}

    public void initialize(Context context){
        mHostContext = context;
        HandlerThread handlerThread = new HandlerThread("PluginManager");
        handlerThread.start();
        mHandler = new PackageHandler(handlerThread.getLooper());
        scanAlreadyInstallPackage();
        installHook();
    }

    public Looper getMainLooper(){
        return mHostContext.getMainLooper();
    }

    public String getHostPackageName(){
        return mHostContext.getPackageName();
    }

    public Context getHostContext() {
        return mHostContext;
    }

    private void scanAlreadyInstallPackage(){}

    private void installHook(){
        installHook(new ActivityManagerHook(mHostContext));
        installHook(new HandleCallbackHook(mHostContext));
        installHook(new PackageManagerHook(mHostContext));
        hookInstrumentation();
    }

    private void hookInstrumentation(){
        Instrumentation instrumentation = (Instrumentation) FieldUtils.readField(ActivityThreadCompat.currentActivityThread(),"mInstrumentation");
        mHookInstrumentation = new HookInstrumentation(instrumentation);
        FieldUtils.writeField(ActivityThreadCompat.currentActivityThread(),"mInstrumentation",mHookInstrumentation);
    }

    private void installHook(Hook hook){
        hook.install();
        mHooks.add(hook);
    }

    public Object getActivityThread(){
        if (mActivityThread == null){
            Context baseContext = (Context) FieldUtils.readField(ContextWrapper.class,mHostContext,"mBase");
            mActivityThread = FieldUtils.readField(baseContext,"mMainThread");
        }
        return mActivityThread;
    }

    public Instrumentation getHookInstrumentation() {
        return mHookInstrumentation;
    }

    public void preparePlugin(Plugin plugin,CompatibilityInfo compatibilityInfo){
        ClassLoader classLoader = plugin.getClassLoader();
        Object loadedApk = MethodUtils.invokeMethod(getActivityThread(),"getPackageInfoNoCheck",
                new Class[]{ApplicationInfo.class, CompatibilityInfo.class},plugin.getApplicationInfo(),compatibilityInfo);
        FieldUtils.writeField(loadedApk,"mClassLoader", classLoader);
        FieldUtils.writeField(loadedApk,"mResources",plugin.getResources());
        MethodUtils.invokeMethod(loadedApk,"makeApplication",new Class[]{boolean.class,Instrumentation.class},
                false,getHookInstrumentation());
    }

    public ActivityInfo getActivityInfo(ComponentName componentName){
        synchronized (mPluginPackages){
            Plugin plugin = mPluginPackages.get(componentName.getPackageName());
            if (plugin != null){
                List<PackageParser.Activity> activities = plugin.getActivities();
                for (PackageParser.Activity activity:activities){
                    if (activity.className.equals(componentName.getClassName())){
                        return activity.info;
                    }
                }
            }
        }
        return null;
    }

    public ApplicationInfo getApplicationInfo(String packageName){
        synchronized (mPluginPackages){
            Plugin plugin = mPluginPackages.get(packageName);
            if (plugin != null){
                return plugin.getApplicationInfo();
            }
            return null;
        }
    }

    public Plugin getPlugin(String packageName){
        synchronized (mPluginPackages){
            return mPluginPackages.get(packageName);
        }
    }

    public PackageInfo getPluginPackageInfo(String packageName){
        synchronized (mPluginPackages){
            Plugin plugin = mPluginPackages.get(packageName);
            if (plugin != null){
                PackageInfo packageInfo = new PackageInfo();
            }
        }
        return null;
    }

    public void installPackage(File apkFile,IPluginInstallObserver observer){
        if (apkFile == null){
            String message = "Plugin file can not null";
            Log.d(TAG,"Plugin error message:"+message);
            if (observer != null){
                observer.onInstallError(INSTALL_FILE_NO_INVALID,message);
            }
            return;
        }
        if (!apkFile.exists()){
            if (observer != null){
                observer.onInstallError(INSTALL_FILE_NO_INVALID,"Plugin file dot exists.");
            }
            return;
        }
        Log.d(TAG,"Plugin file path:"+apkFile.getAbsolutePath());
        InstallParams installParams = new InstallParams(apkFile,observer);
        Message message = Message.obtain(mHandler,MSG_INSTALL);
        message.obj = installParams;
        message.sendToTarget();
    }

    private void notifyInstallFailed(final IPluginInstallObserver observer,
                                     final int errorCode, final String message){
        if (observer == null) return;
        mUiHandler.post(new Runnable() {
            @Override
            public void run() {
                observer.onInstallError(errorCode,message);
            }
        });
    }

    private void installPackageInternal(final InstallParams installParams){
        try {
            synchronized (mPluginPackages){
                final File apkFile = installParams.apkFile;
                PackageParserCompat.IPackageParser packageParser = PackageParserCompat.get();
                PackageParser.Package pkg = packageParser.parsePackage(mHostContext,apkFile,PackageParser.PARSE_MUST_BE_APK);
                if (pkg == null){
                    notifyInstallFailed(installParams.packageInstallObserver,
                            packageParser.getParseError(),"");
                    return;
                }
                final String packageName = pkg.packageName;
                Plugin plugin = mPluginPackages.get(pkg.packageName);
                if (plugin == null){
                    plugin = new Plugin(this,pkg,apkFile);
                    Log.d(TAG,"Application info uid:"+ plugin.getApplicationInfo().uid);
                    mPluginPackages.put(packageName, plugin);
                    logPluginDir(plugin.getApplicationInfo());
                }
                if (installParams.packageInstallObserver != null){
                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            installParams.packageInstallObserver.onInstalledSuccess(packageName);
                        }
                    });
                }
            }
        } catch (final PackageParser.PackageParserException e) {
            notifyInstallFailed(installParams.packageInstallObserver,e.error,e.getMessage());
            Log.d(TAG,"Package parser error:"+installParams.apkFile.getAbsolutePath());
        }
    }

    private void logPluginDir(ApplicationInfo ai){
        Log.d(TAG,String.format("PackageName:%s [dataDir:%s,sourceDir:%s,nativeLibraryDir:%s]",ai.packageName,
                ai.dataDir,ai.sourceDir,ai.nativeLibraryDir));
    }

    public boolean isPlugin(ComponentName componentName){
        return isPlugin(componentName.getPackageName());
    }

    public boolean isPlugin(String packageName){
        return !getHostPackageName().equals(packageName);
    }
}